using System;
using System.Collections.Generic;
using System.Threading;

namespace SharpObjects.MultiThreading
{
    public class ThreadPool<JOB_TYPE, THREAD_CONTEXT_TYPE> : IDisposable
    {
        public class ThreadSelfTerminateException : Exception
        {
            public ThreadSelfTerminateException()
                : base("Thread is self terminating")
            {
            }
        }


        public delegate THREAD_CONTEXT_TYPE InitThreadContextHandler();
        public delegate void ReleaseContextHandler(THREAD_CONTEXT_TYPE context);
        public delegate void ProcessJobHandler(JOB_TYPE job, THREAD_CONTEXT_TYPE context);
        public delegate void ExceptionHandler(Exception ex);
        public delegate JOB_TYPE GetJobHandler();


        public event ExceptionHandler ThreadException;
        public event InitThreadContextHandler InitializeThreadContext;
        public event ReleaseContextHandler ReleaseThreadContext;
        public event ProcessJobHandler ProcessThreadJob;
        public event GetJobHandler GetJob;


        protected int numberOfThreads;
        protected TimeSpan threadSleepInterval;

        protected List<Thread> threads;
        protected List<bool> idleThreads;

        protected int startedCount;
        protected string poolName = "Thread Pool";

        protected Queue<JOB_TYPE> jobQueue = new Queue<JOB_TYPE>();

        protected bool keepRunning;

        protected List<Exception> unhandledExceptions = new List<Exception>();
        protected int maxJobQueueLength = int.MaxValue;

        public ThreadPool(
            int nNumberOfThreads,
            TimeSpan threadSleepInterval
            )
        {
            this.numberOfThreads = nNumberOfThreads;
            this.threadSleepInterval = threadSleepInterval;

            this.threads = new List<Thread>();
            this.idleThreads = new List<bool>();
        }

        public ThreadPool(
            string poolName,
            int nNumberOfThreads,
            TimeSpan threadSleepInterval,
            int maxJobQueueLength
            )
        {
            this.poolName = poolName;

            this.numberOfThreads = nNumberOfThreads;
            this.threadSleepInterval = threadSleepInterval;

            this.threads = new List<Thread>();
            this.idleThreads = new List<bool>();
            this.maxJobQueueLength = maxJobQueueLength;
        }

        public void StartThreads()
        {
            lock (this.threads)
            {
                this.keepRunning = true;
                this.startedCount = 0;

                for (int k = 0; k < this.numberOfThreads; k++)
                {
                    Thread thread;
                    thread = new Thread(new ThreadStart(ThreadFunction));
                    thread.IsBackground = true;
                    thread.Name = string.Format("{0} - thread {1}", this.poolName, k.ToString());
                    this.threads.Add(thread);
                    this.idleThreads.Add(true);
                    thread.Start();
                }
            }
        }

        public Thread[] Threads { get { return this.threads.ToArray(); } }


        public bool StopThreads()
        {
            this.keepRunning = false;

            lock (this.threads)
            {
                for (int k = 0; k < this.threads.Count; k++)
                {
                    // if thread is waiting it will be interrupted
                    // running thread will be interrupted as soon as it 
                    // gets into the waiting state
                    if (this.threads[k] != null)
                    {
                        this.threads[k].Interrupt();
                    }
                }
            }

            // let them finish their job
            for (int k = 0; k < this.threads.Count; k++)
            {
                if (this.threads[k] != null)
                {
                    this.threads[k].Join();
                    this.threads[k] = null;
                }
            }

            return true;
        }

        public void WaitWhileBusy()
        {
            while (this.JobCount > 0 || !this.IsPoolIdle)
            {
                Thread.Sleep(this.threadSleepInterval);
            }
        }

        public int NumberOfThreads { get { return this.numberOfThreads; } }
        public string Name { get { return this.poolName; } set { this.poolName = value; } }

        protected void CallExceptionHandler(Exception ex)
        {
            try
            {
                if (ThreadException != null)
                {
                    ThreadException(ex);
                }
                else
                {
                    lock (this.unhandledExceptions)
                    {
                        this.unhandledExceptions.Add(ex);
                    }
                }
            }
            catch
            {
                lock (this.unhandledExceptions)
                {
                    this.unhandledExceptions.Add(ex);
                }
            }
        }

        public List<Exception> UnhandledExceptions { get { return this.unhandledExceptions; } }

        protected void SetIdle(int threadNumber)
        {
            lock (this.threads)
            {
                this.idleThreads[threadNumber] = true;
            }
        }

        protected void SetWorking(int threadNumber)
        {
            lock (this.threads)
            {
                this.idleThreads[threadNumber] = false;
            }
        }


        protected void ThreadFunction()
        {
            THREAD_CONTEXT_TYPE context = default(THREAD_CONTEXT_TYPE);

            int threadNumber;

            lock (this.threads)
            {
                threadNumber = this.startedCount++;
            }

            try
            {
                if (this.InitializeThreadContext != null)
                {
                    context = this.InitializeThreadContext();
                }
                else
                {
                    context = default(THREAD_CONTEXT_TYPE);
                }


                while (this.keepRunning)
                {
                    try
                    {
                        JOB_TYPE job = GetNextJob();

                        if (job != null)
                        {
                            SetWorking(threadNumber);

                            if (this.ProcessThreadJob != null)
                            {
                                this.ProcessThreadJob(job, context);
                            }
                            else
                            {
                                throw new Exception("No Job Handler delegate or event initialized");
                            }

                        }
                        else
                        {
                            Thread.Sleep(this.threadSleepInterval);
                        }
                    }
                    catch (ThreadAbortException)
                    {
                        break;
                    }
                    catch (ThreadInterruptedException)
                    {
                        break;
                    }
                    catch (ThreadSelfTerminateException)
                    {
                        break;
                    }
                    catch (Exception ex)
                    {
                        CallExceptionHandler(ex);
                        SetIdle(threadNumber);
                        Thread.Sleep(this.threadSleepInterval);
                    }
                    finally
                    {
                        SetIdle(threadNumber);
                    }
                } // while
            }
            catch (Exception ex)
            {
                CallExceptionHandler(ex);
            }
            finally
            {

                try
                {
                    if (this.ReleaseThreadContext != null)
                    {
                        this.ReleaseThreadContext(context);
                    }
                }
                catch (Exception ex)
                {
                    CallExceptionHandler(ex);
                }
                finally
                {
                    this.threads[threadNumber] = null;
                }

                lock (this.threads)
                {
                    this.startedCount--;
                }
            }
        }

        public Queue<JOB_TYPE> JobQueue { get { return this.jobQueue; } }

        public int MaxJobQueueLength { get { return this.maxJobQueueLength; } set { this.maxJobQueueLength = value; } }

        public void AddJob(JOB_TYPE job)
        {
            while (this.JobCount >= maxJobQueueLength)
            {
                Thread.Sleep(this.threadSleepInterval);
            }

            lock (this.jobQueue)
            {
                this.jobQueue.Enqueue(job);
            }
        }

        protected JOB_TYPE GetNextJob()
        {
            if (this.GetJob != null)
            {
                return GetJob();
            }
            else
            {
                lock (this.jobQueue)
                {
                    if (this.jobQueue.Count > 0)
                    {
                        JOB_TYPE job = this.jobQueue.Dequeue();
                        return job;
                    }
                    else
                    {
                        return default(JOB_TYPE);
                    }
                }
            }
        }

        public void PurgeJobs()
        {
            lock (this.jobQueue)
            {
                this.jobQueue.Clear();
            }
        }

        public int JobCount
        {
            get
            {
                lock (this.jobQueue)
                {
                    return this.jobQueue.Count;
                }
            }
        }

        public int ThreadCount
        {
            get
            {
                lock (this.threads)
                {
                    return this.startedCount;
                }
            }
        }

        //private int IdleThreadCount
        //{
        //    get
        //    {
        //        lock (this.threads)
        //        {
        //            int nCount = 0;

        //            for (int k = 0; k < this.idleThreads.Count; k++)
        //            {
        //                if (this.idleThreads[k])
        //                {
        //                    nCount++;
        //                }
        //            }

        //            return nCount;
        //        }
        //    }
        //}

        public bool IsPoolIdle
        {
            get
            {
                lock (this.threads)
                {
                    for (int k = 0; k < this.idleThreads.Count; k++)
                    {
                        if (!this.idleThreads[k])
                        {
                            return false;
                        }
                    }

                    return true;
                }
            }
        }

        public double GetThreadUsage()
        {
            lock (this.threads)
            {
                int nBusyCount = 0;

                for (int k = 0; k < this.idleThreads.Count; k++)
                {
                    if (!this.idleThreads[k])
                    {
                        nBusyCount++;
                    }
                }

                if (this.idleThreads.Count > 0)
                {
                    return (double)nBusyCount / this.idleThreads.Count;
                }
                else
                {
                    return 1.0;
                }
            }
        }

        #region IDisposable Members

        public void Dispose()
        {
            StopThreads();
        }

        #endregion
    }
}
